「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第四篇,最近更新于 2023 年 1 月。
在之前的文章中,我们介绍了什么是 AJAX 技术,以及如何通过客户端提供的 API 向服务端发送请求。在后续的章节中,我们将更讨论另一个非常流行的 AJAX 话题:「跨域获取资源」。
暂看之下,您也许会觉的困惑:什么是域?为什么要跨域?为了了解这一切,让我们先从浏览器的「同源策略」讲起。
1. 同源策略的起源
假如将互联网中的数据比作货物,互联网中的数据流通就像是城市的交通网络,每时每刻都有无数的货车从一个地方到达另一个地方,川流不息。货物是有价值的,信息也是,因此我们需要一种机制,保障信息只被有权限获得的一方获得。所有网络安全的攻防,都是围绕着这个命题而展开。
更具体的来看,这种机制需要保障:
- 客户端可以向服务端请求数据,但服务端可以自定义是否响应;
- 客户端与服务端通信时,数据不会丢失,不会被窃听,篡改;
第二条的具体实践是 TCP 协议和 HTTPS 协议,与我们的主题无关,在此我们不做讨论。我们主要讨论第一条,即在真实的互联网世界中,是什么机制保障了「服务端有条件的响应客户端请求」。
基于 HTTP 协议无状态的特性,我们可以做出一种符合直觉的设计:每个提供 API 的服务端,都通过编码实现一套鉴权机制,根据客户端的登录状态与身份,判断客户端是否有请求权限。
这种设计是行得通的,但无疑会为开发者带来更多底层编码的工作量,并且整个网络世界的信息安全也会由于开发者能力的参差不齐而得不到保障,这显然不是一个理想的方案。
更加理想的方案,便是由各浏览器厂商所共同遵守的「同源策略」。
2. 什么是同源策略
浏览器所共同遵守的「同源策略」是指:限制不同源之间执行特定操作。这涉及到两个概念:什么是「源」?「特定操作」是指什么?接下来让我们逐一说明。
2.1 关于源
在互联网世界,我们约定一个特定的源由以下三部分定义:
- 协议;
- 域名;
- 端口;
当这三者中任意一个发生变化时,在浏览器看就是一个新的源。这个「源」我们可以将其理解为「数据的源头」,不同协议区别了不同的服务方式,不同域名暗示了不同的服务,不同的接口则可能意味着同一服务的不同处理程序,总之任何一个部分发生变化,都可能说明,数据源发生了变化。
而既然数据源发生了变化,理所当然地,我们不应该继续应用默认的数据获取规则。
2.2 关于「特定操作」
这里提到的特定操作是指:
- 读取 Cookie,LocalStorage 和 IndexDB 中的数据;
- 获取 DOM 元素;
- 发送 AJAX 请求;
为什么不能执行这些操作呢?这是一个好问题,为了更好地理解,我们不妨使用逆向思维,思考一下为什么只要源相同,就能执行上述操作。
在绝大多数情况下,客户端的脚本程序是在客户端发送页面请求时,伴随页面加载而加载的。因此我们可以理解为,客户端的脚本来源于特定源,那么脚本在运行中,请求特定源的数据,除非涉及权限问题,否则服务端应该默认放行。
让我们将服务端比作家长,客户端脚本比作家长的子女,那么除非子女要家长的银行卡密码等敏感信息,否则一些合理的需求家长理应满足。
这里需要提到一个需要值得重视,但很多人会忽略的问题,即网页可能会加载第三方脚本,例如 jQuery,那么这相当于将他人的子女请回自己家做事情,这存在一些安全隐患,即如果第三方脚本内包含上述特定操作的代码,则并不会被浏览器同源策略限制。
搞清楚为什么同源状态下,可以执行指定操作,那么为什么在不同源的情况下,不能执行这些操作就变得很清晰了:
- 首先,这些操作关系到用户或服务的数据隐私;
- 当不同源时,相当于要让 A 家庭的子女去索要 B 家庭的物品,那么除非 B 家庭明确表示该物品是可以被任何人或特定家庭共享的,否则拒绝请求这个逻辑就非常天然。
浏览器通过在软件底层使用同源策略,使开发者避免了一部分重复繁琐的权限控制编码工作,增强了网络整体的安全性。
3. 同源策略与安全
您可能还是不太清楚上文提到过得三种操作为什么具有潜在威胁,不必担心,让我为您举例说明。在本章中,请让我扮演一个恶意攻击者,向您说明,如果没有浏览器同源策略,我可以做些什么。
3.1 读取 Cookie,Localstorage 和 IndexDB 中的数据
由于 HTTP 协议无状态的性质,网站通常会将用户登录状态或其他个人信息存储于 Cookie,Localstorage 等区域,并伴随请求发送。那么对于恶意攻击者而言,只要有机会获得这些数据,就可以伪装成数据的真正所有者在网站中进行操作。在没有同源策略的世界,A 网站可以通过 iframe 的方式获取 B 网站的文档对象,假如用户在 B 网站的 cookie 还未过期,那么 A 网站便可以轻松获取用户 cookie 并伪装成用户执行任意操作。
3.2 获取 DOM 元素
用户的交互行为与表单数据都与 DOM 元素强绑定,在没有同源策略的世界,A 网站同样可以通过 iframe 嵌入 B 网站,并追踪记录用户的表单输入,甚至代替用户与 UI 进行交互。这听起来就极具威胁。
3.3 发送 AJAX 请求
为什么要禁止不同源的网站发送 AJAX 请求呢?要回答这个问题,我们需要先明确 Cookie 的特性。这又要说回 HTTP 协议不保留状态的设计。
在现代 Web 应用中,用户状态(例如登录态等)与应用逻辑关系密切,因此 HTTP 协议为了弥补自身无状态的不足,提供了 Cookie 机制,这一机制按照如下模式运作:
当我们设置 Cookie 时,除了存放键值对形式的数据信息外,浏览器还会为 Cookie 的一些属性填充默认值(我们也可以手动修改这些属性的值)。在这些属性中,我们需要关注 domain
和 path
两个属性,它们一个代表域名,一个代表请求路径,两者加起来构成了一个确定这条 Cookie 何时被调用和访问的 URL。与此同时,浏览器自己维护的 Cookie 文件中也会添加一条对应的 Cookie 记录。
当我们在浏览器中发送 HTTP 请求时,浏览器首先会检查请求地址并在自己所维护的 Cookie 文件中寻找匹配的 Cookie 信息,并将其添加到请求头中的 Cookie
属性内,然后再向服务器发送请求。请注意,这个请求头信息是浏览器为了客户端与服务端交互便利偷偷帮我们添加的,很多初级程序员甚至没有意识到这一点。
下面重点来了,当我们的 HTTP 请求到达服务器时,服务器返回的响应中,响应头会原封不动的返回我们发送给他的 Cookie 信息。嗅到危险的味道了吗?我们虽然不能在发送请求前获得 Cookie 信息,但是在发送请求后,我们还是能够获得用户的 Cookie!
让我再举个具体的例子,假设我们在服务器上托管了站点 A,并在其中隐藏了一段脚本,每个登录站点 A 的人都会自动发送 AJAX 请求至站点 B(站点 B 是一个银行网站),那么在没有浏览器同源策略的情况下,如果站点 A 中的访问者恰好在站点 B 的 Cookie 没有失效,那么通过 AJAX 请求返回的响应头信息,我们一样可以拿到这位用户的 Cookie,从而伪装成用户在站点 B 登录,为所欲为。(CSRF攻击即是利用了这个原理,只不过出于同源策略限制,并不能通过发起 AJAX 的方式)。
到这里,您应该能充分意识到为什么这三个特殊操作需要同源策略多加防范。最后再让我们看看同源策略在现实中的表现。
4. 同源策略的表现
从下图中可以看到,当我们在页面中使用 <iframe>
标签时,我们只能获取一个空空的 #document
节点,并没有额外的 DOM 信息。
而当我们发送一个跨域的 AJAX 请求后,打开浏览器的控制台,我们可以发现浏览器其实并没有阻止我们发送请求,而是阻止接收这次请求的响应。也就是说,在同源策略下,服务端实实在在地接收到了这次请求(因此同源策略无法防范 DDOS 攻击),只是响应不包含特定头部而被浏览器拒绝,此时,浏览器会在控制台中打印出下图所示的错误信息。
为什么浏览器不直接拒绝掉所有跨域的 AJAX 请求呢?原因在于在现实世界,服务端提供的 API 并不仅限于同域资源的请求,例如公共 API 就接收所有客户端的数据请求。
另外需要注意的是,对于跨域 AJAX 请求,实际上我们在请求报头也不会看到任何 Cookie 信息,这是因为 CORS 标准中做了规定,浏览器在发送跨域请求时,不能发送任何认证信息。除非您显式的将xhr
对象的withCredentials
属性的值设置为true
并且服务器端也允许客户端请求携带认证信息(即服务器端在响应头中设置了Access-Control-Allow-Credentials: true
)。
5. 小结
至此,您应该已经充分理解了浏览器为什么要执行同源策略,以及同源策略的内容和意义。并且在第四节中,您应该也察觉到在现实生活中,我们其实存在需要跨域请求资源的场景,那么在真实的开发场景中,我们是如何突破浏览器「同源策略」的限制,在不同源的情况下,获取想要的数据呢?
这个问题将带领我们走向下面两个章节,让我为您介绍目前主流的跨域资源获取技术。请稍作休息,我们下一章见。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。